package junior.util.tree;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import junior.util.exception.ExceptionUtils;
import junior.util.reflect.ReflectUtils;
import junior.util.value.ValueUtils;
import lombok.Builder;

@Builder
public class Tree {
	private String idProp;
	private String parentIdProp;
	private String childrenProp;
	private String sortProp;
	private String levelProp;
	private String leafProp;
	private TreeCallback callback;
	
	public String getIdProp() {
		return idProp;
	}
	public void setIdProp(String idProp) {
		this.idProp = idProp;
	}
	public String getParentIdProp() {
		return parentIdProp;
	}
	public void setParentIdProp(String parentIdProp) {
		this.parentIdProp = parentIdProp;
	}
	public String getChildrenProp() {
		return childrenProp;
	}
	public void setChildrenProp(String childrenProp) {
		this.childrenProp = childrenProp;
	}
	public String getSortProp() {
		return sortProp;
	}
	public void setSortProp(String sortProp) {
		this.sortProp = sortProp;
	}
	public String getLevelProp() {
		return levelProp;
	}
	public void setLevelProp(String levelProp) {
		this.levelProp = levelProp;
	}
	public String getLeafProp() {
		return leafProp;
	}
	public void setLeafProp(String leafProp) {
		this.leafProp = leafProp;
	}
	public TreeCallback getCallback() {
		return callback;
	}
	public void setCallback(TreeCallback callback) {
		this.callback = callback;
	}
	
	public static <T> String getTreeString(Collection<T> tree, String nameProp, String childrenProp) {
		return doGetTreeString(tree, nameProp, childrenProp, 1);
	}
	
	private static <T> String doGetTreeString(Collection<T> tree, String nameProp, String childrenProp, int level) {
		if (tree == null || tree.isEmpty()) {
			return "";
		}

		StringBuilder buff = new StringBuilder();
		Class<T> clazz = (Class<T>) tree.iterator().next().getClass();
		
		try {
			Method nameMethod = ReflectUtils.getter(clazz, nameProp);
			Method childrenMethod = ReflectUtils.getter(clazz, childrenProp);
			
			for(T node : tree) {
				int c = level;
				while(c-- > 1) {
					buff.append("  ");
				}
				
				if(level > 1) {
					buff.append("|--");
				}
				
				buff.append(nameMethod.invoke(node) + "\n");
				
				Collection<T> children = (Collection<T>) childrenMethod.invoke(node);
				
				if(children != null && ! children.isEmpty()) {
					buff.append(doGetTreeString(children, nameProp, childrenProp, level + 1));
				}
			}
		} catch (Exception e) {
			throw ExceptionUtils.wrap2Runtime(e);
		}
		
		return buff.toString();
	}
	
	public <T> List<T> toTree(Collection<T> list) {
		if(list.isEmpty()) {
			return new ArrayList<T>();
		}
		
		try {
			Class<T> clazz = (Class<T>) list.iterator().next().getClass();
			Method idGetter = ReflectUtils.getter(clazz, this.getIdProp());
			Method parentIdGetter = ReflectUtils.getter(clazz, this.getParentIdProp());
			Method childrenGetter = ReflectUtils.getter(clazz, this.getChildrenProp());
			Method childrenSetter = ReflectUtils.setter(clazz, this.getChildrenProp(), childrenGetter.getReturnType());
			
			Method sortGetter = null;
			if (this.getSortProp() != null) {
				sortGetter = ReflectUtils.getter(clazz, this.getSortProp());
			}
			Method levelSetter = null;
			if (this.getLevelProp() != null) {
				Class<?> thisType = null; 
				try {
					Field field = ReflectUtils.getDeclaredField(clazz, this.getLevelProp(), true);
					thisType = field.getType();
				} catch (Exception ignore) {}
				thisType = ValueUtils.ifNull(thisType, int.class);
				levelSetter = ReflectUtils.setter(clazz, this.getLevelProp(), thisType);
			}
			Method leafPropSetter = null;
			if (this.getLeafProp() != null) {
				Class<?> thisType = null; 
				try {
					Field field = ReflectUtils.getDeclaredField(clazz, this.getLeafProp(), true);
					thisType = field.getType();
				} catch (Exception ignore) {}
				thisType = ValueUtils.ifNull(thisType, boolean.class);
				leafPropSetter = ReflectUtils.setter(clazz, this.getLeafProp(), thisType);
			}
			
			Map<Object, T> map = new HashMap<Object, T>(list.size());
	    	for(T data : list) {
	    		Object id = idGetter.invoke(data);
	    		map.put(id, data);
	    	}
	    	
	    	List<T> tree = new LinkedList<T>();
	    	
	    	for(T node : list) {
	    		Object parentId = parentIdGetter.invoke(node);
	    		
	    		if(parentId != null && map.containsKey(parentId)) {
	    			T parentNode = map.get(parentId);
	    			
	    			if(this.getCallback() != null) {
	    				this.getCallback().execute(node, parentNode);
	    			}
	    			Collection<T> children = (Collection<T>) childrenGetter.invoke(parentNode);
	    			if (children == null) {
	    				Class<?> returnType = childrenGetter.getReturnType();
	    				if (List.class.isAssignableFrom(childrenGetter.getReturnType())) {
	    					children = new ArrayList<T>();
	    				} else if (Set.class.isAssignableFrom(childrenGetter.getReturnType())) {
	    					children = new LinkedHashSet<T>();
	    				} else {
	    					throw new RuntimeException("Not supported type : "+returnType);
	    				}
	    				childrenSetter.invoke(parentNode, children);
	    			}
	    			children.add(node);
	    		} else {
	    			tree.add(node);
	    		}
	    	}
	    	
	    	assignTreeLevelAndSort(tree, 1, childrenGetter, sortGetter, levelSetter, leafPropSetter);
	    	
	    	return tree;
		} catch(Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	private <T> void assignTreeLevelAndSort(Collection<T> tree, int level, Method childrenGetter,
    		Method sortGetter, Method levelSetter, Method leafSetter) throws Exception {
    	if (sortGetter != null) {
    		if (List.class.isAssignableFrom(tree.getClass())) {
    			sortBranch((List<T>) tree, sortGetter);
    		}
    	}
    	
    	for(T node : tree) {
    		if (levelSetter != null) {
    			levelSetter.invoke(node, level);
    		}
    		
    		Collection<T> children = (Collection<T>) childrenGetter.invoke(node);
    		
    		if(children != null && ! children.isEmpty()) {
    			if (leafSetter != null) {
    				leafSetter.invoke(node, false);
    			}
    			assignTreeLevelAndSort(children, level + 1, childrenGetter, sortGetter, levelSetter, leafSetter);
    		} else {
    			if (leafSetter != null) {
    				leafSetter.invoke(node, true);
    			}
    		}
    	}
    }

	private <T> void sortBranch(List<T> tree, final Method sortGetter) {
		Collections.sort(tree, new Comparator<T>() {
			@SuppressWarnings("rawtypes")
			@Override
			public int compare(T n1, T n2) {
				try {
					Object sortObj1 = sortGetter.invoke(n1);
					Object sortObj2 = sortGetter.invoke(n2);
					
					if (sortObj1 == sortObj2) {
						return -1;
					}
					if (sortObj1 == null || sortObj2 == null) {
						return -1;
					}
					
					Comparable cpr1 = (Comparable) sortObj1;
					Comparable cpr2 = (Comparable) sortObj2;
					
					return cpr1.compareTo(cpr2);
				} catch (Exception e) {
					return 1;
				}
			}
		});
	}

}
